/*
 * Copyright (c) 2002-2003 by OpenSymphony
 * All rights reserved.
 */
package com.opensymphony.workflow.loader;

import com.opensymphony.workflow.InvalidWorkflowDescriptorException;
import com.opensymphony.workflow.util.Validatable;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.*;

/**
 * Describes a single workflow
 * 
 * @author <a href="mailto:plightbo@hotmail.com">Pat Lightbody</a>
 */
public class WorkflowDescriptor extends AbstractDescriptor implements Validatable {
	// ~ Static fields/initializers
	// /////////////////////////////////////////////

	public static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
	public static final String DOCTYPE_DECL = "<!DOCTYPE workflow PUBLIC \"-//OpenSymphony Group//DTD OSWorkflow 2.6//EN\" \"http://dl.bintray.com/trunkplatform/osworkflow/dtd//workflow_2_8.dtd\">";

	// ~ Instance fields
	// ////////////////////////////////////////////////////////

	protected ConditionsDescriptor globalConditions = null;
	protected List commonActionsList = new ArrayList(); // for preserving order
	protected List globalActions = new ArrayList();
	protected List initialActions = new ArrayList();
	protected List joins = new ArrayList();
	protected List registers = new ArrayList();
	protected List splits = new ArrayList();
	protected List steps = new ArrayList();
	protected Map commonActions = new HashMap();
	protected Map metaAttributes = new HashMap();
	protected Map timerFunctions = new HashMap();
	protected String workflowName = null;

	// ~ Constructors
	// ///////////////////////////////////////////////////////////

	/**
	 * @deprecated use {@link DescriptorFactory} instead
	 */
	public WorkflowDescriptor() {
	}

	/**
	 * @deprecated use {@link DescriptorFactory} instead
	 */
	public WorkflowDescriptor(Element root) {
		init(root);
	}

	// ~ Methods
	// ////////////////////////////////////////////////////////////////

	public ActionDescriptor getAction(int id) {
		// check global actions
		for (Iterator iterator = globalActions.iterator(); iterator.hasNext();) {
			ActionDescriptor actionDescriptor = (ActionDescriptor) iterator.next();

			if (actionDescriptor.getId() == id) {
				return actionDescriptor;
			}
		}

		// check steps
		for (Iterator iterator = steps.iterator(); iterator.hasNext();) {
			StepDescriptor stepDescriptor = (StepDescriptor) iterator.next();
			ActionDescriptor actionDescriptor = stepDescriptor.getAction(id);

			if (actionDescriptor != null) {
				return actionDescriptor;
			}
		}

		// check initial actions, which we now must have unique id's
		for (Iterator iterator = initialActions.iterator(); iterator.hasNext();) {
			ActionDescriptor actionDescriptor = (ActionDescriptor) iterator.next();

			if (actionDescriptor.getId() == id) {
				return actionDescriptor;
			}
		}

		return null;
	}

	/**
	 * Get a Map of the common actions specified, keyed on actionId (an Integer)
	 * 
	 * @return A list of {@link ActionDescriptor} objects
	 */
	public Map getCommonActions() {
		return commonActions;
	}

	/**
	 * Get a List of the global actions specified
	 * 
	 * @return A list of {@link ActionDescriptor} objects
	 */
	public List getGlobalActions() {
		return globalActions;
	}

	public ConditionsDescriptor getGlobalConditions() {
		return globalConditions;
	}

	public ActionDescriptor getInitialAction(int id) {
		for (Iterator iterator = initialActions.iterator(); iterator.hasNext();) {
			ActionDescriptor actionDescriptor = (ActionDescriptor) iterator.next();

			if (actionDescriptor.getId() == id) {
				return actionDescriptor;
			}
		}

		return null;
	}

	/**
	 * Get a List of initial steps for this workflow
	 * 
	 * @return A list of {@link ActionDescriptor} objects
	 */
	public List getInitialActions() {
		return initialActions;
	}

	public JoinDescriptor getJoin(int id) {
		for (Iterator iterator = joins.iterator(); iterator.hasNext();) {
			JoinDescriptor joinDescriptor = (JoinDescriptor) iterator.next();

			if (joinDescriptor.getId() == id) {
				return joinDescriptor;
			}
		}

		return null;
	}

	/**
	 * Get a List of initial steps for this workflow
	 * 
	 * @return A list of {@link JoinDescriptor} objects
	 */
	public List getJoins() {
		return joins;
	}

	public Map getMetaAttributes() {
		return metaAttributes;
	}

	public void setName(String name) {
		workflowName = name;
	}

	public String getName() {
		return workflowName;
	}

	public List getRegisters() {
		return registers;
	}

	public SplitDescriptor getSplit(int id) {
		for (Iterator iterator = splits.iterator(); iterator.hasNext();) {
			SplitDescriptor splitDescriptor = (SplitDescriptor) iterator.next();

			if (splitDescriptor.getId() == id) {
				return splitDescriptor;
			}
		}

		return null;
	}

	/**
	 * Get a List of initial steps for this workflow
	 * 
	 * @return A list of {@link SplitDescriptor} objects
	 */
	public List getSplits() {
		return splits;
	}

	public StepDescriptor getStep(int id) {
		for (Iterator iterator = steps.iterator(); iterator.hasNext();) {
			StepDescriptor step = (StepDescriptor) iterator.next();

			if (step.getId() == id) {
				return step;
			}
		}

		return null;
	}

	/**
	 * Get a List of steps in this workflow
	 * 
	 * @return a List of {@link StepDescriptor} objects
	 */
	public List getSteps() {
		return steps;
	}

	/**
	 * Update a trigger function
	 * 
	 * @param id
	 *            The id for the trigger function
	 * @param descriptor
	 *            The descriptor for the trigger function
	 * @return The old trigger function with the specified ID, if any existed
	 */
	public FunctionDescriptor setTriggerFunction(int id, FunctionDescriptor descriptor) {
		return (FunctionDescriptor) timerFunctions.put(new Integer(id), descriptor);
	}

	public FunctionDescriptor getTriggerFunction(int id) {
		return (FunctionDescriptor) this.timerFunctions.get(new Integer(id));
	}

	/**
	 * Get a Map of all trigger functions in this workflow
	 * 
	 * @return a Map with Integer keys and {@link FunctionDescriptor} values
	 */
	public Map getTriggerFunctions() {
		return timerFunctions;
	}

	/**
	 * Add a common action
	 * 
	 * @param descriptor
	 *            The action descriptor to add
	 * @throws IllegalArgumentException
	 *             if the descriptor's ID already exists in the workflow
	 */
	public void addCommonAction(ActionDescriptor descriptor) {
		descriptor.setCommon(true);
		addAction(commonActions, descriptor);
		addAction(commonActionsList, descriptor);
	}

	/**
	 * Add a global action
	 * 
	 * @param descriptor
	 *            The action descriptor to add
	 * @throws IllegalArgumentException
	 *             if the descriptor's ID already exists in the workflow
	 */
	public void addGlobalAction(ActionDescriptor descriptor) {
		addAction(globalActions, descriptor);
	}

	/**
	 * Add an initial action
	 * 
	 * @param descriptor
	 *            The action descriptor to add
	 * @throws IllegalArgumentException
	 *             if the descriptor's ID already exists in the workflow
	 */
	public void addInitialAction(ActionDescriptor descriptor) {
		addAction(initialActions, descriptor);
	}

	/**
	 * Add a join
	 * 
	 * @param descriptor
	 *            The join descriptor to add
	 * @throws IllegalArgumentException
	 *             if the descriptor's ID already exists in the workflow
	 */
	public void addJoin(JoinDescriptor descriptor) {
		if (getJoin(descriptor.getId()) != null) {
			throw new IllegalArgumentException("Join with id " + descriptor.getId() + " already exists");
		}

		joins.add(descriptor);
	}

	/**
	 * Add a split
	 * 
	 * @param descriptor
	 *            The split descriptor to add
	 * @throws IllegalArgumentException
	 *             if the descriptor's ID already exists in the workflow
	 */
	public void addSplit(SplitDescriptor descriptor) {
		if (getSplit(descriptor.getId()) != null) {
			throw new IllegalArgumentException("Split with id " + descriptor.getId() + " already exists");
		}

		splits.add(descriptor);
	}

	/**
	 * Add a step
	 * 
	 * @param descriptor
	 *            The step descriptor to add
	 * @throws IllegalArgumentException
	 *             if the descriptor's ID already exists in the workflow
	 */
	public void addStep(StepDescriptor descriptor) {
		if (getStep(descriptor.getId()) != null) {
			throw new IllegalArgumentException("Step with id " + descriptor.getId() + " already exists");
		}

		steps.add(descriptor);
	}

	/**
	 * Remove an action from this workflow completely.
	 * <p>
	 * This method will check global actions and all steps.
	 * 
	 * @return true if the action was successfully removed, false if it was not
	 *         found
	 */
	public boolean removeAction(ActionDescriptor actionToRemove) {
		// global actions
		for (Iterator iterator = getGlobalActions().iterator(); iterator.hasNext();) {
			ActionDescriptor actionDescriptor = (ActionDescriptor) iterator.next();

			if (actionDescriptor.getId() == actionToRemove.getId()) {
				getGlobalActions().remove(actionDescriptor);

				return true;
			}
		}

		// steps
		for (Iterator iterator = getSteps().iterator(); iterator.hasNext();) {
			StepDescriptor stepDescriptor = (StepDescriptor) iterator.next();
			ActionDescriptor actionDescriptor = stepDescriptor.getAction(actionToRemove.getId());

			if (actionDescriptor != null) {
				stepDescriptor.getActions().remove(actionDescriptor);

				return true;
			}
		}

		return false;
	}

	public void validate() throws InvalidWorkflowDescriptorException {
		ValidationHelper.validate(this.getRegisters());
		ValidationHelper.validate(this.getTriggerFunctions().values());
		ValidationHelper.validate(this.getGlobalActions());
		ValidationHelper.validate(this.getInitialActions());
		ValidationHelper.validate(this.getCommonActions().values());
		ValidationHelper.validate(this.getSteps());
		ValidationHelper.validate(this.getSplits());
		ValidationHelper.validate(this.getJoins());

		Set actions = new HashSet();
		Iterator i = globalActions.iterator();

		while (i.hasNext()) {
			ActionDescriptor action = (ActionDescriptor) i.next();
			actions.add(new Integer(action.getId()));
		}

		i = getSteps().iterator();

		while (i.hasNext()) {
			StepDescriptor step = (StepDescriptor) i.next();
			Iterator j = step.getActions().iterator();

			while (j.hasNext()) {
				ActionDescriptor action = (ActionDescriptor) j.next();

				// check to see if it's a common action (dups are ok, since
				// that's the point of common actions!)
				if (!action.isCommon()) {
					if (!actions.add(new Integer(action.getId()))) {
						throw new InvalidWorkflowDescriptorException("Duplicate occurance of action ID " + action.getId() + " found in step " + step.getId());
					}
				}
			}
		}

		// now we have all our unique actions, let's check that no common action
		// id's exist in them
		i = commonActions.keySet().iterator();

		while (i.hasNext()) {
			Integer action = (Integer) i.next();

			if (actions.contains(action)) {
				throw new InvalidWorkflowDescriptorException("common-action ID " + action + " is duplicated in a step action");
			}
		}

		i = initialActions.iterator();

		while (i.hasNext()) {
			ActionDescriptor action = (ActionDescriptor) i.next();

			if (actions.contains(new Integer(action.getId()))) {
				throw new InvalidWorkflowDescriptorException("initial-action ID " + action + " is duplicated in a step action");
			}
		}

		validateDTD();
	}

	public void writeXML(PrintWriter out, int indent) {
		XMLUtil.printIndent(out, indent++);
		out.println("<workflow>");

		Iterator iter = metaAttributes.entrySet().iterator();

		while (iter.hasNext()) {
			Map.Entry entry = (Map.Entry) iter.next();
			XMLUtil.printIndent(out, indent);
			out.print("<meta name=\"");
			out.print(XMLUtil.encode(entry.getKey()));
			out.print("\">");
			out.print(XMLUtil.encode(entry.getValue()));
			out.println("</meta>");
		}

		if (registers.size() > 0) {
			XMLUtil.printIndent(out, indent++);
			out.println("<registers>");

			for (int i = 0; i < registers.size(); i++) {
				RegisterDescriptor register = (RegisterDescriptor) registers.get(i);
				register.writeXML(out, indent);
			}

			XMLUtil.printIndent(out, --indent);
			out.println("</registers>");
		}

		if (timerFunctions.size() > 0) {
			XMLUtil.printIndent(out, indent++);
			out.println("<trigger-functions>");

			Iterator iterator = timerFunctions.entrySet().iterator();

			while (iterator.hasNext()) {
				Map.Entry entry = (Map.Entry) iterator.next();
				XMLUtil.printIndent(out, indent++);
				out.println("<trigger-function id=\"" + entry.getKey() + "\">");

				FunctionDescriptor trigger = (FunctionDescriptor) entry.getValue();
				trigger.writeXML(out, indent);
				XMLUtil.printIndent(out, --indent);
				out.println("</trigger-function>");
			}

			while (iterator.hasNext()) {
			}

			XMLUtil.printIndent(out, --indent);
			out.println("</trigger-functions>");
		}

		if (getGlobalConditions() != null) {
			XMLUtil.printIndent(out, indent++);
			out.println("<global-conditions>");

			getGlobalConditions().writeXML(out, indent);

			out.println("</global-conditions>");
		}

		XMLUtil.printIndent(out, indent++);
		out.println("<initial-actions>");

		for (int i = 0; i < initialActions.size(); i++) {
			ActionDescriptor action = (ActionDescriptor) initialActions.get(i);
			action.writeXML(out, indent);
		}

		XMLUtil.printIndent(out, --indent);
		out.println("</initial-actions>");

		if (globalActions.size() > 0) {
			XMLUtil.printIndent(out, indent++);
			out.println("<global-actions>");

			for (int i = 0; i < globalActions.size(); i++) {
				ActionDescriptor action = (ActionDescriptor) globalActions.get(i);
				action.writeXML(out, indent);
			}

			XMLUtil.printIndent(out, --indent);
			out.println("</global-actions>");
		}

		if (commonActions.size() > 0) {
			XMLUtil.printIndent(out, indent++);
			out.println("<common-actions>");

			Iterator iterator = getCommonActions().values().iterator();

			while (iterator.hasNext()) {
				ActionDescriptor action = (ActionDescriptor) iterator.next();
				action.writeXML(out, indent);
			}

			XMLUtil.printIndent(out, --indent);
			out.println("</common-actions>");
		}

		XMLUtil.printIndent(out, indent++);
		out.println("<steps>");

		for (int i = 0; i < steps.size(); i++) {
			StepDescriptor step = (StepDescriptor) steps.get(i);
			step.writeXML(out, indent);
		}

		XMLUtil.printIndent(out, --indent);
		out.println("</steps>");

		if (splits.size() > 0) {
			XMLUtil.printIndent(out, indent++);
			out.println("<splits>");

			for (int i = 0; i < splits.size(); i++) {
				SplitDescriptor split = (SplitDescriptor) splits.get(i);
				split.writeXML(out, indent);
			}

			XMLUtil.printIndent(out, --indent);
			out.println("</splits>");
		}

		if (joins.size() > 0) {
			XMLUtil.printIndent(out, indent++);
			out.println("<joins>");

			for (int i = 0; i < joins.size(); i++) {
				JoinDescriptor join = (JoinDescriptor) joins.get(i);
				join.writeXML(out, indent);
			}

			XMLUtil.printIndent(out, --indent);
			out.println("</joins>");
		}

		XMLUtil.printIndent(out, --indent);
		out.println("</workflow>");
	}

	protected void init(Element root) {
		NodeList children = root.getChildNodes();

		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);

			if (child.getNodeName().equals("meta")) {
				Element meta = (Element) child;
				String value = XMLUtil.getText(meta);
				this.metaAttributes.put(meta.getAttribute("name"), value);
			}
		}

		// handle registers - OPTIONAL
		Element r = XMLUtil.getChildElement(root, "registers");

		if (r != null) {
			List registers = XMLUtil.getChildElements(r, "register");

			for (int i = 0; i < registers.size(); i++) {
				Element register = (Element) registers.get(i);
				RegisterDescriptor registerDescriptor = DescriptorFactory.getFactory().createRegisterDescriptor(register);
				registerDescriptor.setParent(this);
				this.registers.add(registerDescriptor);
			}
		}

		// handle global-conditions - OPTIONAL
		Element globalConditionsElement = XMLUtil.getChildElement(root, "global-conditions");

		if (globalConditionsElement != null) {
			Element globalConditions = XMLUtil.getChildElement(globalConditionsElement, "conditions");

			ConditionsDescriptor conditionsDescriptor = DescriptorFactory.getFactory().createConditionsDescriptor(globalConditions);
			conditionsDescriptor.setParent(this);
			this.globalConditions = conditionsDescriptor;
		}

		// handle initial-steps - REQUIRED
		Element intialActionsElement = XMLUtil.getChildElement(root, "initial-actions");
		List initialActions = XMLUtil.getChildElements(intialActionsElement, "action");

		for (int i = 0; i < initialActions.size(); i++) {
			Element initialAction = (Element) initialActions.get(i);
			ActionDescriptor actionDescriptor = DescriptorFactory.getFactory().createActionDescriptor(initialAction);
			actionDescriptor.setParent(this);
			this.initialActions.add(actionDescriptor);
		}

		// handle global-actions - OPTIONAL
		Element globalActionsElement = XMLUtil.getChildElement(root, "global-actions");

		if (globalActionsElement != null) {
			List globalActions = XMLUtil.getChildElements(globalActionsElement, "action");

			for (int i = 0; i < globalActions.size(); i++) {
				Element globalAction = (Element) globalActions.get(i);
				ActionDescriptor actionDescriptor = DescriptorFactory.getFactory().createActionDescriptor(globalAction);
				actionDescriptor.setParent(this);
				this.globalActions.add(actionDescriptor);
			}
		}

		// handle common-actions - OPTIONAL
		// - Store actions in HashMap for now. When parsing Steps, we'll resolve
		// any common actions into local references.
		Element commonActionsElement = XMLUtil.getChildElement(root, "common-actions");

		if (commonActionsElement != null) {
			List commonActions = XMLUtil.getChildElements(commonActionsElement, "action");

			for (int i = 0; i < commonActions.size(); i++) {
				Element commonAction = (Element) commonActions.get(i);
				ActionDescriptor actionDescriptor = DescriptorFactory.getFactory().createActionDescriptor(commonAction);
				actionDescriptor.setParent(this);
				addCommonAction(actionDescriptor);
			}
		}

		// handle timer-functions - OPTIONAL
		Element timerFunctionsElement = XMLUtil.getChildElement(root, "trigger-functions");

		if (timerFunctionsElement != null) {
			List timerFunctions = XMLUtil.getChildElements(timerFunctionsElement, "trigger-function");

			for (int i = 0; i < timerFunctions.size(); i++) {
				Element timerFunction = (Element) timerFunctions.get(i);
				Integer id = new Integer(timerFunction.getAttribute("id"));
				FunctionDescriptor function = DescriptorFactory.getFactory().createFunctionDescriptor(XMLUtil.getChildElement(timerFunction, "function"));
				function.setParent(this);
				this.timerFunctions.put(id, function);
			}
		}

		// handle steps - REQUIRED
		Element stepsElement = XMLUtil.getChildElement(root, "steps");
		List steps = XMLUtil.getChildElements(stepsElement, "step");

		for (int i = 0; i < steps.size(); i++) {
			Element step = (Element) steps.get(i);
			StepDescriptor stepDescriptor = DescriptorFactory.getFactory().createStepDescriptor(step, this);
			this.steps.add(stepDescriptor);
		}

		// handle splits - OPTIONAL
		Element splitsElement = XMLUtil.getChildElement(root, "splits");

		if (splitsElement != null) {
			List split = XMLUtil.getChildElements(splitsElement, "split");

			for (int i = 0; i < split.size(); i++) {
				Element s = (Element) split.get(i);
				SplitDescriptor splitDescriptor = DescriptorFactory.getFactory().createSplitDescriptor(s);
				splitDescriptor.setParent(this);
				this.splits.add(splitDescriptor);
			}
		}

		// handle joins - OPTIONAL:
		Element joinsElement = XMLUtil.getChildElement(root, "joins");

		if (joinsElement != null) {
			List join = XMLUtil.getChildElements(joinsElement, "join");

			for (int i = 0; i < join.size(); i++) {
				Element s = (Element) join.get(i);
				JoinDescriptor joinDescriptor = DescriptorFactory.getFactory().createJoinDescriptor(s);
				joinDescriptor.setParent(this);
				this.joins.add(joinDescriptor);
			}
		}
	}

	// refactored this out from the three addAction methods above
	private void addAction(Object actionsCollectionOrMap, ActionDescriptor descriptor) {
		if (getAction(descriptor.getId()) != null) {
			throw new IllegalArgumentException("action with id " + descriptor.getId() + " already exists for this step.");
		}

		if (actionsCollectionOrMap instanceof Map) {
			((Map) actionsCollectionOrMap).put(new Integer(descriptor.getId()), descriptor);
		} else {
			((Collection) actionsCollectionOrMap).add(descriptor);
		}
	}

	private void validateDTD() throws InvalidWorkflowDescriptorException {
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		dbf.setNamespaceAware(true);
		dbf.setValidating(true);

		StringWriter sw = new StringWriter();
		PrintWriter writer = new PrintWriter(sw);
		writer.println(XML_HEADER);
		writer.println(DOCTYPE_DECL);
		writeXML(writer, 0);

		WorkflowLoader.AllExceptionsErrorHandler errorHandler = new WorkflowLoader.AllExceptionsErrorHandler();

		try {
			DocumentBuilder db = dbf.newDocumentBuilder();
			db.setEntityResolver(new DTDEntityResolver());

			db.setErrorHandler(errorHandler);
			db.parse(new InputSource(new StringReader(sw.toString())));

			if (errorHandler.getExceptions().size() > 0) {
				throw new InvalidWorkflowDescriptorException(errorHandler.getExceptions().toString());
			}
		} catch (InvalidWorkflowDescriptorException e) {
			throw e;
		} catch (Exception e) {
			throw new InvalidWorkflowDescriptorException(e.toString());
		}
	}
}
